package com.codemonkey.lq.qd.builder.xml;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import com.codemonkey.lq.qd.utils.ClassHelper;

import org.apache.ibatis.builder.BuilderException;
import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.slf4j.LoggerFactory;

import com.codemonkey.lq.qd.builder.BaseBuilder;
import com.codemonkey.lq.qd.builder.BuilderEventListener;
import com.codemonkey.lq.qd.builder.BuilderPostProcessor;
import com.codemonkey.lq.qd.extend.impl.SdAutoRefreshEventListener;
import com.codemonkey.lq.qd.facotry.config.ExecuteContextModifier;
import com.codemonkey.lq.qd.facotry.config.ExecuteEventListener;
import com.codemonkey.lq.qd.facotry.config.SConfiguration;
import com.codemonkey.lq.qd.facotry.config.SqlSessionFactorySelecter;
import com.codemonkey.lq.qd.facotry.config.event.SimpleExecuteEventMulticaster;
import com.codemonkey.lq.qd.facotry.config.executer.ServiceHandler;
import com.codemonkey.lq.qd.utils.ClassUtils;
import com.codemonkey.lq.qd.utils.StringHelper;

/**
 * 用于解析全局xml配置文件
 * @author lq
 *
 */
public final class XMLSConfigBuilder extends BaseBuilder {

	private final String resource;
	private XPathParser parser;
	private boolean builded;

	public XMLSConfigBuilder(String globalServiceXMLFile) {
		super(new SConfiguration());
		commonConstructor();

		resource = globalServiceXMLFile;

		InputStream is = XMLSConfigBuilder.class.getClassLoader().getResourceAsStream(
				globalServiceXMLFile);

		if (null == is) {
			throw new IllegalArgumentException("file not found : " + globalServiceXMLFile);
		}

		InputStream resourceAsStream = checkForUtf8BOMAndDiscardIfAny(is);
		XPathParser parser = new XPathParser(resourceAsStream, false, null,
				new XMLMapperEntityResolver());

		this.parser = parser;
	}

	public XMLSConfigBuilder(InputStream is, String globalServiceXMLFile) {
		super(new SConfiguration());
		commonConstructor();

		resource = globalServiceXMLFile;

		InputStream resourceAsStream = checkForUtf8BOMAndDiscardIfAny(is);
		XPathParser parser = new XPathParser(resourceAsStream, false, null,
				new XMLMapperEntityResolver());

		this.parser = parser;
	}

	/**
	 * 系统内部使用的扩展,进行预加载
	 * TODO 这个到底应该放在哪里??
	 */
	private void commonConstructor() {
		configuration.addBuilderPostProcessor(new XMLServiceBuilderPostProcessor());
		configuration.addBuilderEventListener(SimpleExecuteEventMulticaster.getSingleton());
	}

	public SConfiguration build() {
		SConfiguration parse = (SConfiguration) parse();
		return parse;
	}

	@Override
	protected Object doParse() {
		if (builded) {
			throw new IllegalArgumentException("Each XMLSConfigBuilder can only be used once.");
		}
		builded = true;
		parseConfiguration(parser.evalNode("/configuration"));
		return configuration;
	}

	private void parseConfiguration(XNode root) {
		try {
			propertiesElement(root.evalNode("properties"));
			settingsElement(root.evalNode("settings"));
			serviceHandlerElement(root.evalNode("Interceptors"));
			serviceElement(root.evalNode("services"));

		} catch (Exception e) {
			throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
		}
	}

	/**
	 * 直接抄自MyBatis的XMLConfigBuilder中的propertiesElement方法
	 * @param propertiesContext
	 * @throws Exception
	 */
	//解析<property name="" value="">
	private void propertiesElement(XNode propertiesContext) throws Exception {
		if (propertiesContext != null) {
			Properties defaults = propertiesContext.getChildrenAsProperties();
			String resource = propertiesContext.getStringAttribute("resource");
			String url = propertiesContext.getStringAttribute("url");
			if (resource != null && url != null) {
				throw new BuilderException(
						"The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
			}
			if (resource != null) {
				defaults.putAll(Resources.getResourceAsProperties(resource));
			} else if (url != null) {
				defaults.putAll(Resources.getUrlAsProperties(url));
			}
			// 这个目前就没必要的了,对应mybatis里的Variables
			//		      Properties vars = configuration.getVariables();
			//		      if (vars != null) {
			//		        defaults.putAll(vars);
			//		      }
			parser.setVariables(defaults);
			configuration.setVariables(defaults);
		}
	}

	// 解析 <settings />
	private void settingsElement(XNode context) throws Exception {
		if (null == context) {
			throw new IllegalArgumentException("settings can not be null");
		}

		Properties props = context.getChildrenAsProperties();
		instanceSqlSessionFactorySelecter(props);
		instanceExecuteContextModifier(props); // must before	
		instanceBuilderPostProcessors(props);
		instanceBuilderEventListener(props);
		instanceExecuteEventListener(props);

		autoRefreshIfNeed(props);

	}

	private void instanceSqlSessionFactorySelecter(Properties settings) {
		String prop_sqlSessionFactorySelecter = settings.getProperty("sqlSessionFactorySelecter");
		if (StringHelper.isEmpty(prop_sqlSessionFactorySelecter)) {
			throw new IllegalArgumentException("[sqlSessionFactorySelecter] setting must set");
		}

		SqlSessionFactorySelecter ssfs = ClassHelper
				.<SqlSessionFactorySelecter> newInstance(prop_sqlSessionFactorySelecter);
		configuration.setSqlSessionFactorySelecter(ssfs);
	}

	private void instanceExecuteContextModifier(Properties settings) {
		String prop_executeContextModifier = settings.getProperty("executeContextModifier");
		if (StringHelper.isEmpty(prop_executeContextModifier)) {
			return;
		}

		ExecuteContextModifier ecModifier = ClassHelper
				.<ExecuteContextModifier> newInstance(prop_executeContextModifier);
		configuration.setExecuteContextModifier(ecModifier);

	}

	private void instanceBuilderPostProcessors(Properties settings) {
		String prop_builderPostProcessor = settings.getProperty("builderPostProcessor");
		if (StringHelper.isEmpty(prop_builderPostProcessor)) {
			return;
		}

		StringTokenizer st = new StringTokenizer(prop_builderPostProcessor, ",+;", false);
		while (st.hasMoreElements()) {
			String cls = st.nextToken();
			BuilderPostProcessor bpp = ClassHelper.<BuilderPostProcessor> newInstance(cls);

			if (bpp == null) {
				String errorInfo = String
						.format(
								"the class '%s' does not exist or not implement BuilderPostProcessor interface",
								cls);
				throw new IllegalArgumentException(errorInfo);
			}

			configuration.addBuilderPostProcessor(bpp);

		}
	}

	private void instanceBuilderEventListener(Properties settings) {
		String prop_builderEventListener = settings.getProperty("buildEventListener");
		if (StringHelper.isEmpty(prop_builderEventListener)) {
			return;
		}

		//允许使用,或+或;作为间隔来添加多个监听器
		StringTokenizer st = new StringTokenizer(prop_builderEventListener, ",+;", false);
		while (st.hasMoreElements()) {
			//外界配置的
			BuilderEventListener listenerConfiged = ClassHelper
					.<BuilderEventListener> newInstance(st.nextToken());

			configuration.addBuilderEventListener(listenerConfiged);
		}
	}

	private void instanceExecuteEventListener(Properties settings) {
		String prop_builderEventListener = settings.getProperty("executeEventListener");
		if (StringHelper.isEmpty(prop_builderEventListener)) {
			return;
		}

		//允许使用,或+或;作为间隔来添加多个监听器
		StringTokenizer st = new StringTokenizer(prop_builderEventListener, ",+;", false);
		while (st.hasMoreElements()) {
			//外界配置的
			ExecuteEventListener listenerConfiged = ClassHelper
					.<ExecuteEventListener> newInstance(st.nextToken());

			configuration.addExecuteEventListener(listenerConfiged);
		}
	}

	private void autoRefreshIfNeed(Properties settings) {
		Boolean isDebug = Boolean.parseBoolean(settings.getProperty("isDebug"));
		if (!isDebug) {
			return;
		}

		configuration.addBuilderEventListener(new SdAutoRefreshEventListener());
	}

	private void serviceHandlerElement(XNode interceptorsNode) {
		if (interceptorsNode == null) {
			return;
		}

		for (XNode child : interceptorsNode.evalNodes("Interceptor")) {
			// child   :  <Interceptor class = "xx">
			String cls = child.getStringAttribute("class");
			ServiceHandler sh = ClassHelper.<ServiceHandler> newInstance(cls);

			if (sh == null) {
				String errorInfo = String.format(
						"the class '%s' does not exist or not implement ServiceHandler interface",
						cls);
				throw new IllegalArgumentException(errorInfo);
			}

			configuration.addServiceHandler(sh);
		}
	}

	//解析<services>节点
	private void serviceElement(XNode servicesContext) throws Exception {
		if (servicesContext == null) {
			return;
		}

		for (XNode child : servicesContext.getChildren()) {
			if ("include".equals(child.getName())) {
				String resourcePath = child.getStringAttribute("resource");
				doParseServiceElement(configuration, resourcePath);
			} else {
				//暂未考虑其他形式的来源,目前只考虑的xml file形式的
			}
		}
	}

	//解析<include resource="xxx">
	private void doParseServiceElement(SConfiguration configuration, String resourcePath)
			throws Exception {
		if (!configuration.isResourceLoaded(resourcePath)) {
			InputStream inputStream = Resources.getResourceAsStream(resourcePath);
			servicesElement(new XPathParser(inputStream).evalNode("/service-list"));
			configuration.addLoadedResource(resourcePath);

			LoggerFactory.getLogger(XMLSConfigBuilder.class).debug("service file {0} has parsed",
					resourcePath);
		}
	}

	private void servicesElement(XNode context) {
		try {
			//buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
			buildServiceFromContext(context.evalNodes("service"));
		} catch (Exception e) {
			throw new RuntimeException("Error parsing Mapper XML. Cause: " + e, e);
		}
	}

	private void buildServiceFromContext(List<XNode> list) {
		for (XNode context : list) {
			final XMLServiceBuilder builder = new XMLServiceBuilder(context, configuration,
					resource);
			builder.parse();
		}
	}

	//过滤BOM
	private static InputStream checkForUtf8BOMAndDiscardIfAny(InputStream inputStream) {
		PushbackInputStream pushbackInputStream = new PushbackInputStream(new BufferedInputStream(
				inputStream), 3);
		byte[] bom = new byte[3];

		try {
			if (pushbackInputStream.read(bom) != -1) {
				if (!(bom[0] == (byte) 0xEF && bom[1] == (byte) 0xBB && bom[2] == (byte) 0xBF)) {
					pushbackInputStream.unread(bom);
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}

		return pushbackInputStream;
	}

}
